package p.ithorns.sample.influxdb.mapper;

import com.influxdb.annotations.Column;
import com.influxdb.exceptions.InfluxException;
import com.influxdb.query.FluxRecord;
import org.jetbrains.annotations.NotNull;
import p.ithorns.sample.influxdb.core.BeanUtil;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * FluxResultBeanMapper
 * 参考和重写FluxResultMapper
 * 增加class缓存实现
 *
 * @author HtL
 * @date 2025/1/17 10:50
 * @see com.influxdb.query.internal.FluxResultMapper
 * @since 1.0.0
 */
public class FluxResultBeanMapper {

    // we don't cover some corner cases(e.g. someURI someHRName) under which explicit mapping is required
    private static final Pattern CAMEL_CASE_TO_SNAKE_CASE_REPLACE_PATTERN = Pattern.compile("[A-Z]");

    @NotNull
    public <T> T toBean(@NotNull FluxRecord record, @NotNull Class<T> clazz) {
        Objects.requireNonNull(record, "Record is required");
        Objects.requireNonNull(clazz, "Class type is required");

        Map<String, Field> classFields = BeanUtil.getClassFields(clazz);
        T pojo = BeanUtil.instance(clazz);
        Class<?> currentClazz = clazz;
        while (currentClazz != null) {
            Set<Map.Entry<String, Field>> entries = classFields.entrySet();
            for (Map.Entry<String, Field> entry : entries) {
                String columnName = entry.getKey();
                Field field = entry.getValue();
                Column anno = field.getAnnotation(Column.class);
                if (anno != null && !anno.name().isEmpty()) {
                    columnName = anno.name();
                }

                Map<String, Object> recordValues = record.getValues();

                String col = null;

                if (recordValues.containsKey(columnName)) {
                    col = columnName;
                } else if (recordValues.containsKey("_" + columnName)) {
                    col = "_" + columnName;
                } else if (anno != null && anno.measurement()) {
                    col = "_measurement";
                } else {
                    String columnNameInSnakeCase = camelCaseToSnakeCase(columnName);
                    if (recordValues.containsKey(columnNameInSnakeCase)) {
                        col = columnNameInSnakeCase;
                    }
                }

                if (col != null) {
                    Object value = record.getValueByKey(col);
                    setFieldValue(pojo, field, value);
                }
            }

            currentClazz = currentClazz.getSuperclass();
        }
        return pojo;
    }

    private String camelCaseToSnakeCase(final String str) {
        return CAMEL_CASE_TO_SNAKE_CASE_REPLACE_PATTERN.matcher(str)
                .replaceAll("_$0")
                .toLowerCase();
    }

    private void setFieldValue(@Nonnull final Object object,
                               @Nullable final Field field,
                               @Nullable final Object value) {

        if (field == null || value == null) {
            return;
        }

        try {
            Class<?> fieldType = field.getType();

            //the same type
            if (fieldType.equals(value.getClass())) {
                field.set(object, value);
                return;
            }

            //convert primitives
            if (double.class.isAssignableFrom(fieldType) || Double.class.isAssignableFrom(fieldType)) {
                field.set(object, toDoubleValue(value));
                return;
            }
            if (long.class.isAssignableFrom(fieldType) || Long.class.isAssignableFrom(fieldType)) {
                field.set(object, toLongValue(value));
                return;
            }
            if (int.class.isAssignableFrom(fieldType) || Integer.class.isAssignableFrom(fieldType)) {
                field.set(object, toIntValue(value));
                return;
            }
            if (float.class.isAssignableFrom(fieldType) || Float.class.isAssignableFrom(fieldType)) {
                field.set(object, toFloatValue(value));
                return;
            }
            if (short.class.isAssignableFrom(fieldType) || Short.class.isAssignableFrom(fieldType)) {
                field.set(object, toShortValue(value));
                return;
            }
            if (boolean.class.isAssignableFrom(fieldType)) {
                field.setBoolean(object, Boolean.parseBoolean(String.valueOf(value)));
                return;
            }
            if (BigDecimal.class.isAssignableFrom(fieldType)) {
                field.set(object, toBigDecimalValue(value));
                return;
            }

            //enum
            if (fieldType.isEnum()) {
                //noinspection unchecked, rawtypes
                field.set(object, Enum.valueOf((Class<Enum>) fieldType, String.valueOf(value)));
                return;
            }

            field.set(object, value);

        } catch (ClassCastException | IllegalAccessException e) {
            String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. "
                    + "The correct type is '%s' (current field value: '%s').";
            throw new InfluxException(String.format(msg, object.getClass().getName(), field.getName(),
                    value.getClass().getName(), value));
        }
    }

    private double toDoubleValue(final Object value) {
        if (double.class.isAssignableFrom(value.getClass()) || Double.class.isAssignableFrom(value.getClass())) {
            return (double) value;
        }

        return ((Number) value).doubleValue();
    }

    private long toLongValue(final Object value) {
        if (long.class.isAssignableFrom(value.getClass()) || Long.class.isAssignableFrom(value.getClass())) {
            return (long) value;
        }

        return ((Number) value).longValue();
    }

    private int toIntValue(final Object value) {
        if (int.class.isAssignableFrom(value.getClass()) || Integer.class.isAssignableFrom(value.getClass())) {
            return (int) value;
        }

        return ((Number) value).intValue();
    }

    private float toFloatValue(final Object value) {
        if (float.class.isAssignableFrom(value.getClass()) || Float.class.isAssignableFrom(value.getClass())) {
            return (float) value;
        }

        return ((Number) value).floatValue();
    }

    private short toShortValue(final Object value) {
        if (short.class.isAssignableFrom(value.getClass()) || Short.class.isAssignableFrom(value.getClass())) {
            return (short) value;
        }

        return ((Number) value).shortValue();
    }

    private BigDecimal toBigDecimalValue(final Object value) {
        if (String.class.isAssignableFrom(value.getClass())) {
            return new BigDecimal((String) value);
        }

        if (double.class.isAssignableFrom(value.getClass()) || Double.class.isAssignableFrom(value.getClass())) {
            return BigDecimal.valueOf((double) value);
        }

        if (int.class.isAssignableFrom(value.getClass()) || Integer.class.isAssignableFrom(value.getClass())) {
            return BigDecimal.valueOf((int) value);
        }

        if (long.class.isAssignableFrom(value.getClass()) || Long.class.isAssignableFrom(value.getClass())) {
            return BigDecimal.valueOf((long) value);
        }

        if (float.class.isAssignableFrom(value.getClass()) || Float.class.isAssignableFrom(value.getClass())) {
            return BigDecimal.valueOf((float) value);
        }

        if (short.class.isAssignableFrom(value.getClass()) || Short.class.isAssignableFrom(value.getClass())) {
            return BigDecimal.valueOf((short) value);
        }

        String message = String.format("Cannot cast %s [%s] to %s.",
                value.getClass().getName(), value, BigDecimal.class);
        throw new ClassCastException(message);
    }

}